﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ThreadLearn
{
    public static class TaskLogger
    {
        public enum TaskLogLevel { None, Pending }
        public static TaskLogLevel LogLevel { get; set; }

        public sealed class TaskLogEntry
        {
            public Task Task { get; internal set; }
            public String Tag { get; internal set; }
            public DateTime LogTime { get; internal set; }
            public String CallerMemberName { get; internal set; }
            public String CallerFilePath { get; internal set; }
            public Int32 CallerLineNumber { get; internal set; }
            public override string ToString()
            {
                return String.Format("LogTime={0},Tag={1},Member={2},File={3}({4})", LogTime, Tag, CallerMemberName, CallerFilePath, CallerLineNumber);
            }
        }

        private static readonly ConcurrentDictionary<Task, TaskLogEntry> s_log = new ConcurrentDictionary<Task, TaskLogEntry>();
        public static IEnumerable<TaskLogEntry> GetLogEntries() { return s_log.Values; }

        public static Task<TResult> Log<TResult>(this Task<TResult> task, String tag = null,
            [CallerMemberName] String callerMemberName = null,
            [CallerFilePath] String callerPathName = null,
            [CallerLineNumber] Int32 callerLineNumber = -1
            )
        {
            return (Task<TResult>)Log((Task)task, tag, callerMemberName, callerPathName, callerLineNumber);
        }

        public static Task Log(this Task task, String tag = null,
            [CallerMemberName] String callerMemberName = null,
            [CallerFilePath] String callerPathName = null,
            [CallerLineNumber] Int32 callerLineNumber = -1)
        {
            if (LogLevel == TaskLogLevel.None) return task;
            var logEntry = new TaskLogEntry
            {
                Task = task,
                LogTime = DateTime.Now,
                Tag = tag,
                CallerMemberName = callerMemberName,
                CallerFilePath = callerPathName,
                CallerLineNumber = callerLineNumber
            };
            s_log[task] = logEntry;
            task.ContinueWith(t =>
            {
                TaskLogEntry entry;
                s_log.TryRemove(t, out entry);
            }, TaskContinuationOptions.ExecuteSynchronously);

            

            return task;
        }

        public static async Task Go()
        {
#if DEBUG
            TaskLogger.LogLevel = TaskLogger.TaskLogLevel.Pending;
#endif
            var tasks = new List<Task>
                    {
                        Task.Delay(2000).Log("2s op"),
                        Task.Delay(5000).Log("5s op"),
                        Task.Delay(6000).Log("6s op")
                    };
            try
            {
                await Task.WhenAll(tasks).WithCancellation(new CancellationTokenSource(3000).Token);
            }
            catch (OperationCanceledException) { }

            foreach (var op in TaskLogger.GetLogEntries().OrderBy(tle => tle.LogTime))
                Console.WriteLine(op);
        }


        private struct Void { }

        private static async Task<TResult> WithCancellation<TResult>(this Task<TResult> originalTask, CancellationToken ct)
        {
            var cancelTask = new TaskCompletionSource<Void>();
            using (ct.Register(t => ((TaskCompletionSource<Void>)t).TrySetResult(new Void()), cancelTask)) {
                Task any = await Task.WhenAny(originalTask, cancelTask.Task);

                if (any == cancelTask.Task) ct.ThrowIfCancellationRequested();
            }
            return await originalTask;
        }

        private static async Task WithCancellation(this Task originalTask, CancellationToken ct)
        {
            var cancelTask = new TaskCompletionSource<Void>();
            using (ct.Register(t => ((TaskCompletionSource<Void>)t).TrySetResult(new Void()), cancelTask))
            {
                Task any = await Task.WhenAny(originalTask, cancelTask.Task);

                if (any == cancelTask.Task) ct.ThrowIfCancellationRequested();
            }
            await originalTask;
        }

        public static async Task Go1()
        {
            var cts = new CancellationTokenSource(2000);
            var ct = cts.Token;
            try
            {
                await Task.Delay(1000).WithCancellation(ct);
                Console.WriteLine("Task completed");
            }
            catch(OperationCanceledException)
            {
                Console.WriteLine("Task canceled");
            }
        }

    }
}
